/*
 ***************************************************************************************
 *  Copyright (C) 2006 EsperTech, Inc. All rights reserved.                            *
 *  http://www.espertech.com/esper                                                     *
 *  http://www.espertech.com                                                           *
 *  ---------------------------------------------------------------------------------- *
 *  The software in this package is published under the terms of the GPL license       *
 *  a copy of which has been included with this distribution in the license.txt file.  *
 ***************************************************************************************
 */
package com.espertech.esper.common.client.soda;

import java.io.Serializable;
import java.io.StringWriter;
import java.util.List;

/**
 * Object model of an EPL statement.
 * <p>
 * Applications can create an object model by instantiating this class and then setting the various clauses.
 * When done, use the administrative interface to deploy from the model.
 * <p>
 * Use the toEPL method to generate a textual EPL from an object model.
 * <p>
 * Minimally, and EPL statement consists of the select-clause and the where-clause. These are represented by {@link SelectClause}
 * and {@link FromClause} respectively.
 * <p>
 * Here is a short example that create a simple EPL statement such as "select page, responseTime from PageLoad" :
 * <pre>
 * EPStatementObjectModel model = new EPStatementObjectModel();
 * model.setSelectClause(SelectClause.create("page", "responseTime"));
 * model.setPropertyEvalSpec(FromClause.create(FilterStream.create("PageLoad")));
 * </pre>
 * <p>
 * The select-clause and from-clause must be set for the statement object model to be useable by the
 * administrative API. All other clauses a optional.
 * <p>
 * Please see the documentation set for further examples.
 */
public class EPStatementObjectModel implements Serializable {
    private static final long serialVersionUID = 0L;

    private List<AnnotationPart> annotations;
    private List<ExpressionDeclaration> expressionDeclarations;
    private List<ScriptExpression> scriptExpressions;
    private List<ClassProvidedExpression> classProvidedExpressions;
    private String contextName;
    private UpdateClause updateClause;
    private CreateVariableClause createVariable;
    private CreateTableClause createTable;
    private CreateWindowClause createWindow;
    private CreateIndexClause createIndex;
    private CreateSchemaClause createSchema;
    private CreateContextClause createContext;
    private CreateDataFlowClause createDataFlow;
    private CreateExpressionClause createExpression;
    private CreateClassClause createClass;
    private OnClause onExpr;
    private InsertIntoClause insertInto;
    private SelectClause selectClause;
    private FromClause fromClause;
    private Expression whereClause;
    private GroupByClause groupByClause;
    private Expression havingClause;
    private OutputLimitClause outputLimitClause;
    private OrderByClause orderByClause;
    private RowLimitClause rowLimitClause;
    private MatchRecognizeClause matchRecognizeClause;
    private ForClause forClause;
    private String treeObjectName;
    private FireAndForgetClause fireAndForgetClause;
    private IntoTableClause intoTableClause;

    /**
     * Ctor.
     */
    public EPStatementObjectModel() {
    }

    /**
     * Specify an insert-into-clause.
     *
     * @param insertInto specifies the insert-into-clause, or null to indicate that the clause is absent
     */
    public void setInsertInto(InsertIntoClause insertInto) {
        this.insertInto = insertInto;
    }

    /**
     * Specify an insert-into-clause.
     *
     * @param insertInto specifies the insert-into-clause, or null to indicate that the clause is absent
     * @return model
     */
    public EPStatementObjectModel insertInto(InsertIntoClause insertInto) {
        this.insertInto = insertInto;
        return this;
    }

    /**
     * Return the insert-into-clause, or null to indicate that the clause is absent.
     *
     * @return specification of the insert-into-clause, or null if none present
     */
    public InsertIntoClause getInsertInto() {
        return insertInto;
    }

    /**
     * Specify a select-clause.
     *
     * @param selectClause specifies the select-clause, the select-clause cannot be null and must be set
     */
    public void setSelectClause(SelectClause selectClause) {
        this.selectClause = selectClause;
    }

    /**
     * Specify a select-clause.
     *
     * @param selectClause specifies the select-clause, the select-clause cannot be null and must be set
     * @return model
     */
    public EPStatementObjectModel selectClause(SelectClause selectClause) {
        this.selectClause = selectClause;
        return this;
    }

    /**
     * Return the select-clause.
     *
     * @return specification of the select-clause
     */
    public SelectClause getSelectClause() {
        return selectClause;
    }

    /**
     * Specify a from-clause.
     *
     * @param fromClause specifies the from-clause, the from-clause cannot be null and must be set
     */
    public void setFromClause(FromClause fromClause) {
        this.fromClause = fromClause;
    }

    /**
     * Specify a from-clause.
     *
     * @param fromClause specifies the from-clause, the from-clause cannot be null and must be set
     * @return model
     */
    public EPStatementObjectModel fromClause(FromClause fromClause) {
        this.fromClause = fromClause;
        return this;
    }

    /**
     * Return the where-clause, or null to indicate that the clause is absent.
     *
     * @return specification of the where-clause, or null if none present
     */
    public Expression getWhereClause() {
        return whereClause;
    }

    /**
     * Specify a where-clause.
     *
     * @param whereClause specifies the where-clause, which is optional and can be null
     */
    public void setWhereClause(Expression whereClause) {
        this.whereClause = whereClause;
    }

    /**
     * Specify a where-clause.
     *
     * @param whereClause specifies the where-clause, which is optional and can be null
     * @return model
     */
    public EPStatementObjectModel whereClause(Expression whereClause) {
        this.whereClause = whereClause;
        return this;
    }

    /**
     * Return the from-clause.
     *
     * @return specification of the from-clause
     */
    public FromClause getFromClause() {
        return fromClause;
    }

    /**
     * Return the group-by-clause, or null to indicate that the clause is absent.
     *
     * @return specification of the group-by-clause, or null if none present
     */
    public GroupByClause getGroupByClause() {
        return groupByClause;
    }

    /**
     * Specify a group-by-clause.
     *
     * @param groupByClause specifies the group-by-clause, which is optional and can be null
     */
    public void setGroupByClause(GroupByClause groupByClause) {
        this.groupByClause = groupByClause;
    }

    /**
     * Specify a group-by-clause.
     *
     * @param groupByClause specifies the group-by-clause, which is optional and can be null
     * @return model
     */
    public EPStatementObjectModel groupByClause(GroupByClause groupByClause) {
        this.groupByClause = groupByClause;
        return this;
    }

    /**
     * Return the having-clause, or null to indicate that the clause is absent.
     *
     * @return specification of the having-clause, or null if none present
     */
    public Expression getHavingClause() {
        return havingClause;
    }

    /**
     * Specify a having-clause.
     *
     * @param havingClause specifies the having-clause, which is optional and can be null
     */
    public void setHavingClause(Expression havingClause) {
        this.havingClause = havingClause;
    }

    /**
     * Specify a having-clause.
     *
     * @param havingClause specifies the having-clause, which is optional and can be null
     * @return model
     */
    public EPStatementObjectModel havingClause(Expression havingClause) {
        this.havingClause = havingClause;
        return this;
    }

    /**
     * Return the order-by-clause, or null to indicate that the clause is absent.
     *
     * @return specification of the order-by-clause, or null if none present
     */
    public OrderByClause getOrderByClause() {
        return orderByClause;
    }

    /**
     * Specify an order-by-clause.
     *
     * @param orderByClause specifies the order-by-clause, which is optional and can be null
     */
    public void setOrderByClause(OrderByClause orderByClause) {
        this.orderByClause = orderByClause;
    }

    /**
     * Specify an order-by-clause.
     *
     * @param orderByClause specifies the order-by-clause, which is optional and can be null
     * @return model
     */
    public EPStatementObjectModel orderByClause(OrderByClause orderByClause) {
        this.orderByClause = orderByClause;
        return this;
    }

    /**
     * Return the output-rate-limiting-clause, or null to indicate that the clause is absent.
     *
     * @return specification of the output-rate-limiting-clause, or null if none present
     */
    public OutputLimitClause getOutputLimitClause() {
        return outputLimitClause;
    }

    /**
     * Specify an output-rate-limiting-clause.
     *
     * @param outputLimitClause specifies the output-rate-limiting-clause, which is optional and can be null
     */
    public void setOutputLimitClause(OutputLimitClause outputLimitClause) {
        this.outputLimitClause = outputLimitClause;
    }

    /**
     * Specify an output-rate-limiting-clause.
     *
     * @param outputLimitClause specifies the output-rate-limiting-clause, which is optional and can be null
     * @return model
     */
    public EPStatementObjectModel outputLimitClause(OutputLimitClause outputLimitClause) {
        this.outputLimitClause = outputLimitClause;
        return this;
    }

    /**
     * Renders the object model in it's EPL syntax textual representation.
     *
     * @return EPL representing the statement object model
     * @throws IllegalStateException if required clauses do not exist
     */
    public String toEPL() {
        StringWriter writer = new StringWriter();
        toEPL(new EPStatementFormatter(false), writer);
        return writer.toString();
    }

    /**
     * Rendering using the provided writer.
     *
     * @param writer to use
     */
    public void toEPL(StringWriter writer) {
        toEPL(new EPStatementFormatter(false), writer);
    }

    /**
     * Rendering using the provided formatter.
     *
     * @param formatter to use
     * @return rendered string
     */
    public String toEPL(EPStatementFormatter formatter) {
        StringWriter writer = new StringWriter();
        toEPL(formatter, writer);
        return writer.toString();
    }

    /**
     * Renders the object model in it's EPL syntax textual representation, using a whitespace-formatter as provided.
     *
     * @param formatter the formatter to use
     * @param writer    writer to use
     * @throws IllegalStateException if required clauses do not exist
     */
    public void toEPL(EPStatementFormatter formatter, StringWriter writer) {
        AnnotationPart.toEPL(writer, annotations, formatter);
        ExpressionDeclaration.toEPL(writer, expressionDeclarations, formatter);
        ScriptExpression.toEPL(writer, scriptExpressions, formatter);
        ClassProvidedExpression.toEPL(writer, classProvidedExpressions, formatter);

        if (contextName != null) {
            formatter.beginContext(writer);
            writer.append("context ");
            writer.append(contextName);
        }

        if (createIndex != null) {
            formatter.beginCreateIndex(writer);
            createIndex.toEPL(writer);
            return;
        } else if (createSchema != null) {
            formatter.beginCreateSchema(writer);
            createSchema.toEPL(writer);
            return;
        } else if (createExpression != null) {
            formatter.beginCreateExpression(writer);
            createExpression.toEPL(writer);
            return;
        } else if (createClass != null) {
            formatter.beginCreateExpression(writer);
            createClass.toEPL(writer);
            return;
        } else if (createContext != null) {
            formatter.beginCreateContext(writer);
            createContext.toEPL(writer, formatter);
            return;
        } else if (createWindow != null) {
            formatter.beginCreateWindow(writer);
            createWindow.toEPL(writer);

            writer.write(" as ");
            if ((selectClause == null) || (selectClause.getSelectList().isEmpty()) && !createWindow.getColumns().isEmpty()) {
                createWindow.toEPLCreateTablePart(writer);
            } else {
                selectClause.toEPL(writer, formatter, false, false);
                if (createWindow.getAsEventTypeName() != null) {
                    writer.append(" from ");
                    writer.append(createWindow.getAsEventTypeName());
                }
                createWindow.toEPLInsertPart(writer);
            }
            return;
        } else if (createVariable != null) {
            formatter.beginCreateVariable(writer);
            createVariable.toEPL(writer);
            return;
        } else if (createTable != null) {
            formatter.beginCreateTable(writer);
            createTable.toEPL(writer);
            return;
        } else if (createDataFlow != null) {
            formatter.beginCreateDataFlow(writer);
            createDataFlow.toEPL(writer, formatter);
            return;
        }

        boolean displayWhereClause = true;
        if (updateClause != null) {
            formatter.beginUpdate(writer);
            updateClause.toEPL(writer);
        } else if (onExpr != null) {
            formatter.beginOnTrigger(writer);
            writer.write("on ");
            fromClause.getStreams().get(0).toEPL(writer, formatter);

            if (onExpr instanceof OnDeleteClause) {
                formatter.beginOnDelete(writer);
                writer.write("delete from ");
                ((OnDeleteClause) onExpr).toEPL(writer);
            } else if (onExpr instanceof OnUpdateClause) {
                formatter.beginOnUpdate(writer);
                writer.write("update ");
                ((OnUpdateClause) onExpr).toEPL(writer);
            } else if (onExpr instanceof OnSelectClause) {
                OnSelectClause onSelect = (OnSelectClause) onExpr;
                if (insertInto != null) {
                    insertInto.toEPL(writer, formatter, true);
                }
                selectClause.toEPL(writer, formatter, true, onSelect.isDeleteAndSelect());
                writer.write(" from ");
                onSelect.toEPL(writer);
            } else if (onExpr instanceof OnSetClause) {
                OnSetClause onSet = (OnSetClause) onExpr;
                onSet.toEPL(writer, formatter);
            } else if (onExpr instanceof OnMergeClause) {
                OnMergeClause merge = (OnMergeClause) onExpr;
                merge.toEPL(writer, whereClause, formatter);
                displayWhereClause = false;
            } else {
                OnInsertSplitStreamClause split = (OnInsertSplitStreamClause) onExpr;
                insertInto.toEPL(writer, formatter, true);
                selectClause.toEPL(writer, formatter, true, false);
                if (whereClause != null) {
                    writer.write(" where ");
                    whereClause.toEPL(writer, ExpressionPrecedenceEnum.MINIMUM);
                }
                split.toEPL(writer, formatter);
                displayWhereClause = false;
            }
        } else {
            if (intoTableClause != null) {
                intoTableClause.toEPL(writer);
            }

            if (selectClause == null) {
                throw new IllegalStateException("Select-clause has not been defined");
            }
            if (fromClause == null) {
                throw new IllegalStateException("From-clause has not been defined");
            }

            if (fireAndForgetClause instanceof FireAndForgetUpdate) {
                FireAndForgetUpdate update = (FireAndForgetUpdate) fireAndForgetClause;
                writer.append("update ");
                fromClause.toEPLOptions(writer, formatter, false);
                writer.append(" ");
                UpdateClause.renderEPLAssignments(writer, update.getAssignments());
            } else if (fireAndForgetClause instanceof FireAndForgetInsert) {
                FireAndForgetInsert insert = (FireAndForgetInsert) fireAndForgetClause;
                insertInto.toEPL(writer, formatter, true);
                if (insert.isUseValuesKeyword()) {
                    writer.append(" values (");
                    String delimiter = "";
                    for (SelectClauseElement element : selectClause.getSelectList()) {
                        writer.write(delimiter);
                        element.toEPLElement(writer);
                        delimiter = ", ";
                    }
                    writer.append(")");
                } else {
                    selectClause.toEPL(writer, formatter, true, false);
                }
            } else if (fireAndForgetClause instanceof FireAndForgetDelete) {
                writer.append("delete ");
                fromClause.toEPLOptions(writer, formatter, true);
            } else {
                if (insertInto != null) {
                    insertInto.toEPL(writer, formatter, true);
                }
                selectClause.toEPL(writer, formatter, true, false);
                fromClause.toEPLOptions(writer, formatter, true);
            }
        }

        if (matchRecognizeClause != null) {
            matchRecognizeClause.toEPL(writer);
        }
        if (whereClause != null && displayWhereClause) {
            formatter.beginWhere(writer);
            writer.write("where ");
            whereClause.toEPL(writer, ExpressionPrecedenceEnum.MINIMUM);
        }
        if (groupByClause != null) {
            formatter.beginGroupBy(writer);
            writer.write("group by ");
            groupByClause.toEPL(writer);
        }
        if (havingClause != null) {
            formatter.beginHaving(writer);
            writer.write("having ");
            havingClause.toEPL(writer, ExpressionPrecedenceEnum.MINIMUM);
        }
        if (outputLimitClause != null) {
            formatter.beginOutput(writer);
            writer.write("output ");
            outputLimitClause.toEPL(writer);
        }
        if (orderByClause != null) {
            formatter.beginOrderBy(writer);
            writer.write("order by ");
            orderByClause.toEPL(writer);
        }
        if (rowLimitClause != null) {
            formatter.beginLimit(writer);
            writer.write("limit ");
            rowLimitClause.toEPL(writer);
        }
        if (forClause != null) {
            formatter.beginFor(writer);
            forClause.toEPL(writer);
        }
    }

    /**
     * Returns the create-window clause for creating named windows, or null if this statement does not
     * create a named window.
     *
     * @return named window creation clause
     */
    public CreateWindowClause getCreateWindow() {
        return createWindow;
    }

    /**
     * Sets the create-window clause for creating named windows, or null if this statement does not
     * create a named window.
     *
     * @param createWindow is the named window creation clause
     */
    public void setCreateWindow(CreateWindowClause createWindow) {
        this.createWindow = createWindow;
    }

    /**
     * Returns the on-delete clause for deleting from named windows, or null if this statement
     * does not delete from a named window
     *
     * @return on delete clause
     */
    public OnClause getOnExpr() {
        return onExpr;
    }

    /**
     * Sets the on-delete or on-select clause for selecting or deleting from named windows, or null if this statement
     * does not on-select or on-delete from a named window
     *
     * @param onExpr is the on-expression (on-select and on-delete) clause to set
     */
    public void setOnExpr(OnClause onExpr) {
        this.onExpr = onExpr;
    }

    /**
     * Returns the create-variable clause if this is a statement creating a variable, or null if not.
     *
     * @return create-variable clause
     */
    public CreateVariableClause getCreateVariable() {
        return createVariable;
    }

    /**
     * Sets the create-variable clause if this is a statement creating a variable, or null if not.
     *
     * @param createVariable create-variable clause
     */
    public void setCreateVariable(CreateVariableClause createVariable) {
        this.createVariable = createVariable;
    }

    /**
     * Returns the row limit specification, or null if none supplied.
     *
     * @return row limit spec if any
     */
    public RowLimitClause getRowLimitClause() {
        return rowLimitClause;
    }

    /**
     * Sets the row limit specification, or null if none applicable.
     *
     * @param rowLimitClause row limit spec if any
     */
    public void setRowLimitClause(RowLimitClause rowLimitClause) {
        this.rowLimitClause = rowLimitClause;
    }

    /**
     * Returns the update specification.
     *
     * @return update spec if defined
     */
    public UpdateClause getUpdateClause() {
        return updateClause;
    }

    /**
     * Sets the update specification.
     *
     * @param updateClause update spec if defined
     */
    public void setUpdateClause(UpdateClause updateClause) {
        this.updateClause = updateClause;
    }

    /**
     * Returns annotations.
     *
     * @return annotations
     */
    public List<AnnotationPart> getAnnotations() {
        return annotations;
    }

    /**
     * Sets annotations.
     *
     * @param annotations to set
     */
    public void setAnnotations(List<AnnotationPart> annotations) {
        this.annotations = annotations;
    }

    /**
     * Match-recognize clause.
     *
     * @return clause
     */
    public MatchRecognizeClause getMatchRecognizeClause() {
        return matchRecognizeClause;
    }

    /**
     * Sets match-recognize clause.
     *
     * @param clause to set
     */
    public void setMatchRecognizeClause(MatchRecognizeClause clause) {
        this.matchRecognizeClause = clause;
    }

    /**
     * Returns create-index clause.
     *
     * @return clause
     */
    public CreateIndexClause getCreateIndex() {
        return createIndex;
    }

    /**
     * Sets create-index clause.
     *
     * @param createIndex to set
     */
    public void setCreateIndex(CreateIndexClause createIndex) {
        this.createIndex = createIndex;
    }

    /**
     * Returns the create-schema clause.
     *
     * @return clause
     */
    public CreateSchemaClause getCreateSchema() {
        return createSchema;
    }

    /**
     * Sets the create-schema clause.
     *
     * @param createSchema clause to set
     */
    public void setCreateSchema(CreateSchemaClause createSchema) {
        this.createSchema = createSchema;
    }

    /**
     * Returns the create-context clause.
     *
     * @return clause
     */
    public CreateContextClause getCreateContext() {
        return createContext;
    }

    /**
     * Sets the create-context clause.
     *
     * @param createContext clause to set
     */
    public void setCreateContext(CreateContextClause createContext) {
        this.createContext = createContext;
    }

    /**
     * Returns the for-clause.
     *
     * @return for-clause
     */
    public ForClause getForClause() {
        return forClause;
    }

    /**
     * Sets the for-clause.
     *
     * @param forClause for-clause
     */
    public void setForClause(ForClause forClause) {
        this.forClause = forClause;
    }

    /**
     * Returns the expression declarations, if any.
     *
     * @return expression declarations
     */
    public List<ExpressionDeclaration> getExpressionDeclarations() {
        return expressionDeclarations;
    }

    /**
     * Sets the expression declarations, if any.
     *
     * @param expressionDeclarations expression declarations to set
     */
    public void setExpressionDeclarations(List<ExpressionDeclaration> expressionDeclarations) {
        this.expressionDeclarations = expressionDeclarations;
    }

    /**
     * Returns the context name if context dimensions apply to statement.
     *
     * @return context name
     */
    public String getContextName() {
        return contextName;
    }

    /**
     * Sets the context name if context dimensions apply to statement.
     *
     * @param contextName context name
     */
    public void setContextName(String contextName) {
        this.contextName = contextName;
    }

    /**
     * Returns the scripts defined.
     *
     * @return scripts
     */
    public List<ScriptExpression> getScriptExpressions() {
        return scriptExpressions;
    }

    /**
     * Sets the scripts.
     *
     * @param scriptExpressions to set
     */
    public void setScriptExpressions(List<ScriptExpression> scriptExpressions) {
        this.scriptExpressions = scriptExpressions;
    }

    public List<ClassProvidedExpression> getClassProvidedExpressions() {
        return classProvidedExpressions;
    }

    public void setClassProvidedExpressions(List<ClassProvidedExpression> classProvidedExpressions) {
        this.classProvidedExpressions = classProvidedExpressions;
    }

    /**
     * Returns the "create dataflow" part, if present.
     *
     * @return create dataflow clause
     */
    public CreateDataFlowClause getCreateDataFlow() {
        return createDataFlow;
    }

    /**
     * Sets the "create dataflow" part,.
     *
     * @param createDataFlow create dataflow clause
     */
    public void setCreateDataFlow(CreateDataFlowClause createDataFlow) {
        this.createDataFlow = createDataFlow;
    }

    /**
     * Returns the internal expression id assigned for tools to identify the expression.
     *
     * @return object name
     */
    public String getTreeObjectName() {
        return treeObjectName;
    }

    /**
     * Sets an internal expression id assigned for tools to identify the expression.
     *
     * @param treeObjectName object name
     */
    public void setTreeObjectName(String treeObjectName) {
        this.treeObjectName = treeObjectName;
    }

    /**
     * Returns the create-expression clause, if any
     *
     * @return clause
     */
    public CreateExpressionClause getCreateExpression() {
        return createExpression;
    }

    /**
     * Sets the create-expression clause, if any
     *
     * @param createExpression clause
     */
    public void setCreateExpression(CreateExpressionClause createExpression) {
        this.createExpression = createExpression;
    }

    public CreateClassClause getCreateClass() {
        return createClass;
    }

    public void setCreateClass(CreateClassClause createClass) {
        this.createClass = createClass;
    }

    /**
     * Returns fire-and-forget (on-demand) query information for FAF select, insert, update and delete.
     *
     * @return fire and forget query information
     */
    public FireAndForgetClause getFireAndForgetClause() {
        return fireAndForgetClause;
    }

    /**
     * Sets fire-and-forget (on-demand) query information for FAF select, insert, update and delete.
     *
     * @param fireAndForgetClause fire and forget query information
     */
    public void setFireAndForgetClause(FireAndForgetClause fireAndForgetClause) {
        this.fireAndForgetClause = fireAndForgetClause;
    }

    /**
     * Returns the into-table clause, or null if none found.
     *
     * @return into-table clause
     */
    public IntoTableClause getIntoTableClause() {
        return intoTableClause;
    }

    /**
     * Sets the into-table clause, or null if none found.
     *
     * @param intoTableClause into-table clause
     */
    public void setIntoTableClause(IntoTableClause intoTableClause) {
        this.intoTableClause = intoTableClause;
    }

    /**
     * Returns the create-table clause if present or null if not present
     *
     * @return create-table clause
     */
    public CreateTableClause getCreateTable() {
        return createTable;
    }

    /**
     * Sets the create-table clause if present or null if not present
     *
     * @param createTable create-table clause
     */
    public void setCreateTable(CreateTableClause createTable) {
        this.createTable = createTable;
    }
}
